今天我們來了解與時間有關的型別(在Polars中習慣稱呼其為temporal型別)及操作。
Polars共有四種temporal型別:
2025-09-01,內部儲存型態為pl.Int32,代表自UNIX epoch(即UTC1970年1月1日0時0分0秒)以來的天數。2025-09-01 07:00:00,內部儲存型態為pl.Int64,代表自UNIX epoch以來的秒數,可以選擇三種秒數單位,分別為us、ns及ms,預設值為us(microseconds)。timedelta。本日大綱如下:
pl.DataFrame.group_by_dynamic()
pl.DataFrame.upsample()
codepanda
使用多個生成時間序列的函數,來建立df dataframe。
以pl.date_range()為例,其共有五個參數:
start=:起始日期。end=:結束日期。interval=:時間間隔。closed=:是否包括起始與結束日期。共有both(預設)、left及right三種。eager=:預設為False,代表不立即生成,而是返回expr。如果設定為True,則會立即生成,並返回series。from datetime import date, datetime, time, timedelta
import pandas as pd
import polars as pl
df = pl.DataFrame(
{
"date": pl.date_range(
date(2025, 1, 1), date(2025, 6, 1), interval="1mo", eager=True
),
"date_str": [
"2025-07-05",
"2025-08-05",
"2025-09-10",
"2025-10-10",
"2025-11-20",
"2025-12-20",
],
"datetime": pl.datetime_range(
datetime(2025, 1, 1),
datetime(2025, 1, 2),
interval="4h",
closed="left",
eager=True,
),
"datetime_utc": pl.datetime_range(
datetime(2025, 1, 1),
datetime(2025, 6, 1),
interval="1mo",
eager=True,
time_zone="UTC",
),
"time": pl.time_range(
time(13, 0, 0),
time(13, 25, 0),
interval=timedelta(minutes=5),
eager=True,
),
}
)
shape: (6, 5)
┌────────────┬────────────┬─────────────────────┬─────────────────────────┬
│ date ┆ date_str ┆ datetime ┆ datetime_utc ┆
│ --- ┆ --- ┆ --- ┆ --- ┆
│ date ┆ str ┆ datetime[μs] ┆ datetime[μs, UTC] ┆
╞════════════╪════════════╪═════════════════════╪═════════════════════════╪
│ 2025-01-01 ┆ 2025-07-05 ┆ 2025-01-01 00:00:00 ┆ 2025-01-01 00:00:00 UTC ┆
│ 2025-02-01 ┆ 2025-08-05 ┆ 2025-01-01 04:00:00 ┆ 2025-02-01 00:00:00 UTC ┆
│ 2025-03-01 ┆ 2025-09-10 ┆ 2025-01-01 08:00:00 ┆ 2025-03-01 00:00:00 UTC ┆
│ 2025-04-01 ┆ 2025-10-10 ┆ 2025-01-01 12:00:00 ┆ 2025-04-01 00:00:00 UTC ┆
│ 2025-05-01 ┆ 2025-11-20 ┆ 2025-01-01 16:00:00 ┆ 2025-05-01 00:00:00 UTC ┆
│ 2025-06-01 ┆ 2025-12-20 ┆ 2025-01-01 20:00:00 ┆ 2025-06-01 00:00:00 UTC ┆
└────────────┴────────────┴─────────────────────┴─────────────────────────┴
┬──────────┐
┆ time │
┆ --- │
┆ time │
╪══════════╡
┆ 13:00:00 │
┆ 13:05:00 │
┆ 13:10:00 │
┆ 13:15:00 │
┆ 13:20:00 │
┆ 13:25:00 │
┴──────────┘
以下我們將透過幾段程式碼,認識temporal的各種型別及dt命名空間提供的各種功能。
pl.Date轉換為pl.String下面這段程式碼展示了:
pl.String型別。temporal型別中的時間資訊時,需要加上(),也就是需要使用pl.Expr.dt.month()。習慣使用Pandas的朋友,常常會寫成pl.Expr.dt.month。pl.Expr.cast將「"date"」列轉換為pl.Int32型別,可以得到其距離UNIX epoch的天數。pl.Expr.cast將「"datetime"」列轉換為pl.Int64型別,可以得到其距離UNIX epoch的秒數(單位為microseconds)。(
df.select(
pl.col("date"),
pl.col("date").dt.strftime("%Y/%m/%d").alias("strftime"),
pl.col("date").dt.month().alias("month"), # not `dt.month`
pl.col("date").cast(pl.Int32).alias("date_in_days"),
pl.col("datetime").cast(pl.Int64).alias("datetime_in_microsecs"),
)
)
shape: (6, 5)
┌────────────┬────────────┬───────┬──────────────┬───────────────────────┐
│ date ┆ strftime ┆ month ┆ date_in_days ┆ datetime_in_microsecs │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ str ┆ i8 ┆ i32 ┆ i64 │
╞════════════╪════════════╪═══════╪══════════════╪═══════════════════════╡
│ 2025-01-01 ┆ 2025/01/01 ┆ 1 ┆ 20089 ┆ 1735689600000000 │
│ 2025-02-01 ┆ 2025/02/01 ┆ 2 ┆ 20120 ┆ 1735704000000000 │
│ 2025-03-01 ┆ 2025/03/01 ┆ 3 ┆ 20148 ┆ 1735718400000000 │
│ 2025-04-01 ┆ 2025/04/01 ┆ 4 ┆ 20179 ┆ 1735732800000000 │
│ 2025-05-01 ┆ 2025/05/01 ┆ 5 ┆ 20209 ┆ 1735747200000000 │
│ 2025-06-01 ┆ 2025/06/01 ┆ 6 ┆ 20240 ┆ 1735761600000000 │
└────────────┴────────────┴───────┴──────────────┴───────────────────────┘
pl.String轉換為pl.Date下面這段程式碼展示了:
pl.Date型別。pl.Duration型別。(
df.with_columns(
pl.col("date_str").str.to_date().alias("to_date"),
)
.with_columns(
pl.col("date").sub(pl.col("to_date")).alias("duration"),
)
.select(
pl.col("date_str", "to_date", "duration"),
pl.col("duration").dt.total_days().alias("duration_days"),
pl.col("duration").dt.total_hours().alias("duration_hours"),
)
)
shape: (6, 5)
┌────────────┬────────────┬──────────────┬───────────────┬────────────────┐
│ date_str ┆ to_date ┆ duration ┆ duration_days ┆ duration_hours │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ date ┆ duration[ms] ┆ i64 ┆ i64 │
╞════════════╪════════════╪══════════════╪═══════════════╪════════════════╡
│ 2025-07-05 ┆ 2025-07-05 ┆ -185d ┆ -185 ┆ -4440 │
│ 2025-08-05 ┆ 2025-08-05 ┆ -185d ┆ -185 ┆ -4440 │
│ 2025-09-10 ┆ 2025-09-10 ┆ -193d ┆ -193 ┆ -4632 │
│ 2025-10-10 ┆ 2025-10-10 ┆ -192d ┆ -192 ┆ -4608 │
│ 2025-11-20 ┆ 2025-11-20 ┆ -203d ┆ -203 ┆ -4872 │
│ 2025-12-20 ┆ 2025-12-20 ┆ -202d ┆ -202 ┆ -4848 │
└────────────┴────────────┴──────────────┴───────────────┴────────────────┘
pl.Date/DateTime與pl.Time為pl.Datetime下面這段程式碼展示了:
pl.Expr.cast將「"time"」列轉換為pl.Int64型別,可以得到其距離午夜的秒數(單位為nanoseconds)。(
df.select(
"date",
"time",
pl.col("date")
.dt.combine(pl.col("time"))
.alias("combined_datetime"),
pl.col("time").cast(pl.Int64).alias("time_in_nanosecs"),
)
)
shape: (6, 4)
┌────────────┬──────────┬─────────────────────┬──────────────────┐
│ date ┆ time ┆ combined_datetime ┆ time_in_nanosecs │
│ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ time ┆ datetime[μs] ┆ i64 │
╞════════════╪══════════╪═════════════════════╪══════════════════╡
│ 2025-01-01 ┆ 13:00:00 ┆ 2025-01-01 13:00:00 ┆ 46800000000000 │
│ 2025-02-01 ┆ 13:05:00 ┆ 2025-02-01 13:05:00 ┆ 47100000000000 │
│ 2025-03-01 ┆ 13:10:00 ┆ 2025-03-01 13:10:00 ┆ 47400000000000 │
│ 2025-04-01 ┆ 13:15:00 ┆ 2025-04-01 13:15:00 ┆ 47700000000000 │
│ 2025-05-01 ┆ 13:20:00 ┆ 2025-05-01 13:20:00 ┆ 48000000000000 │
│ 2025-06-01 ┆ 13:25:00 ┆ 2025-06-01 13:25:00 ┆ 48300000000000 │
└────────────┴──────────┴─────────────────────┴──────────────────┘
Python的zoneinfo.available_timezones()可以列出Polars支援的時區:
import zoneinfo
print(zoneinfo.available_timezones())
下面這段程式碼展示了:
(
df.select(
pl.col("datetime_utc"),
pl.col("datetime_utc")
.dt.convert_time_zone("Asia/Taipei")
.alias("convert_tz_tpe"),
pl.col("datetime_utc")
.dt.replace_time_zone("Asia/Taipei")
.alias("replace_tz_tpe"),
)
)
shape: (6, 3)
┌─────────────────────────┬───────────────────────────┬
│ datetime_utc ┆ convert_tz_tpe ┆
│ --- ┆ --- ┆
│ datetime[μs, UTC] ┆ datetime[μs, Asia/Taipei] ┆
╞═════════════════════════╪═══════════════════════════╪
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 08:00:00 CST ┆
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 08:00:00 CST ┆
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 08:00:00 CST ┆
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 08:00:00 CST ┆
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 08:00:00 CST ┆
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 08:00:00 CST ┆
└─────────────────────────┴───────────────────────────┴
┬───────────────────────────┐
┆ replace_tz_tpe │
┆ --- │
┆ datetime[μs, Asia/Taipei] │
╪═══════════════════════════╡
┆ 2025-01-01 00:00:00 CST │
┆ 2025-02-01 00:00:00 CST │
┆ 2025-03-01 00:00:00 CST │
┆ 2025-04-01 00:00:00 CST │
┆ 2025-05-01 00:00:00 CST │
┆ 2025-06-01 00:00:00 CST │
┴───────────────────────────
請留意,Pl.Expr.dt.convert_time_zone()是實際上對pl.Datetime中的時區資訊進行轉換,而Pl.Expr.dt.replace_time_zone()則是將pl.Datetime中的時區資訊取代(也可以想成指定)為另一個時區。
依照這個邏輯,我們既然可以使用Pl.Expr.dt.replace_time_zone()指定時區,那麼應該也可以將時區資訊自pl.Datetime刪去。實際上,如果將None傳入Pl.Expr.dt.replace_time_zone()的確可以達成這樣的效果:
(
df.select(
pl.col("datetime_utc"),
pl.col("datetime_utc")
.dt.replace_time_zone(None)
.alias("no_tz"),
)
)
shape: (6, 2)
┌─────────────────────────┬─────────────────────┐
│ datetime_utc ┆ no_tz │
│ --- ┆ --- │
│ datetime[μs, UTC] ┆ datetime[μs] │
╞═════════════════════════╪═════════════════════╡
│ 2025-01-01 00:00:00 UTC ┆ 2025-01-01 00:00:00 │
│ 2025-02-01 00:00:00 UTC ┆ 2025-02-01 00:00:00 │
│ 2025-03-01 00:00:00 UTC ┆ 2025-03-01 00:00:00 │
│ 2025-04-01 00:00:00 UTC ┆ 2025-04-01 00:00:00 │
│ 2025-05-01 00:00:00 UTC ┆ 2025-05-01 00:00:00 │
│ 2025-06-01 00:00:00 UTC ┆ 2025-06-01 00:00:00 │
└─────────────────────────┴─────────────────────┘
pl.DataFrame.group_by_dynamic()pl.DataFrame.group_by_dynamic()可以讓我們依據想要的時間間隔進行分組聚合。例如,我們可以將「"date"」列依照兩個月一次的頻率分組(註2),將「"time"」列中的pl.Time收集為pl.List:
df.group_by_dynamic("date", every="2mo").agg(pl.col("time"))
shape: (3, 2)
┌────────────┬──────────────────────┐
│ date ┆ time │
│ --- ┆ --- │
│ date ┆ list[time] │
╞════════════╪══════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└────────────┴──────────────────────┘
除了指定時間間隔的every=參數外,另一個常用的period=參數可以指定聚合時所使用的時間段。例如,我們可以將「"date"」列,依照兩個月一次的頻率分組,並以三個月做聚合計算,收集「"time"」列中的pl.Time:
(
df.group_by_dynamic("date", every="2mo", period="3mo").agg(
pl.col("time")
)
)
shape: (3, 2)
┌────────────┬────────────────────────────────┐
│ date ┆ time │
│ --- ┆ --- │
│ date ┆ list[time] │
╞════════════╪════════════════════════════════╡
│ 2025-01-01 ┆ [13:00:00, 13:05:00, 13:10:00] │
│ 2025-03-01 ┆ [13:10:00, 13:15:00, 13:20:00] │
│ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└────────────┴────────────────────────────────┘
事實上,在不指定period=時,其值會與every=相等,也就是分組與聚合使用一樣的時間間隔。
此外,df.group_by_dynamic()有一個常被大家忽略的好用參數group_by=,可以幫助我們在針對temporal型別做分組時,同時對其它列或expr也進行分組。例如我們可以同時對「"date"」列及pl.col("date_str").str.slice(-2)進行分組聚合:
(
df.group_by_dynamic(
"date", every="2mo", group_by=pl.col("date_str").str.slice(-2)
).agg(pl.col("time"))
)
shape: (3, 3)
┌──────────┬────────────┬──────────────────────┐
│ date_str ┆ date ┆ time │
│ --- ┆ --- ┆ --- │
│ str ┆ date ┆ list[time] │
╞══════════╪════════════╪══════════════════════╡
│ 05 ┆ 2025-01-01 ┆ [13:00:00, 13:05:00] │
│ 10 ┆ 2025-03-01 ┆ [13:10:00, 13:15:00] │
│ 20 ┆ 2025-05-01 ┆ [13:20:00, 13:25:00] │
└──────────┴────────────┴──────────────────────┘
pl.DataFrame.upsample()pl.DataFrame.upsample()可以幫助我們提高時間間隔取樣頻率。例如,我們可以將「"date"」列由四小時間隔提高為兩小時:
with pl.Config(tbl_rows=20):
print(
df.select("datetime", "date_str").upsample("datetime", every="2h")
)
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime ┆ date_str │
│ --- ┆ --- │
│ datetime[μs] ┆ str │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ null │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ null │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ null │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ null │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ null │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘
當然,Polars沒辦法無中生有,提高取樣頻率後的值預設為null,需要由使用者填入適當的值,例如使用pl.Expr.fill_null(),以「"forward"」作為strategy=參數,填補缺失值:
with pl.Config(tbl_rows=20):
print(
df.select("datetime", "date_str")
.upsample("datetime", every="2h")
.fill_null(strategy="forward")
)
shape: (11, 2)
┌─────────────────────┬────────────┐
│ datetime ┆ date_str │
│ --- ┆ --- │
│ datetime[μs] ┆ str │
╞═════════════════════╪════════════╡
│ 2025-01-01 00:00:00 ┆ 2025-07-05 │
│ 2025-01-01 02:00:00 ┆ 2025-07-05 │
│ 2025-01-01 04:00:00 ┆ 2025-08-05 │
│ 2025-01-01 06:00:00 ┆ 2025-08-05 │
│ 2025-01-01 08:00:00 ┆ 2025-09-10 │
│ 2025-01-01 10:00:00 ┆ 2025-09-10 │
│ 2025-01-01 12:00:00 ┆ 2025-10-10 │
│ 2025-01-01 14:00:00 ┆ 2025-10-10 │
│ 2025-01-01 16:00:00 ┆ 2025-11-20 │
│ 2025-01-01 18:00:00 ┆ 2025-11-20 │
│ 2025-01-01 20:00:00 ┆ 2025-12-20 │
└─────────────────────┴────────────┘
codepanda相比於Polars,Pandas在時間相關型別提供了好用的offset alias。
舉例來說,假如我們想知道此次鐵人賽報名時間的每個星期三,分別是開始報名後的第幾天,可以使用W-WED作為pd.DataFrame.resample()的規則。其中W代表以每週為resample目標,而WED則代表以星期三為分界。
idx = pd.date_range("2025-08-01", "2025-09-15")
df_pd = pd.DataFrame({"n": range(1, idx.size + 1)}).set_index(idx)
n
2025-08-01 1
2025-08-02 2
2025-08-03 3
2025-08-04 4
...
2025-09-12 43
2025-09-13 44
2025-09-14 45
2025-09-15 46
df_pd.resample("W-WED").max()
n
2025-08-06 6
2025-08-13 13
2025-08-20 20
2025-08-27 27
2025-09-03 34
2025-09-10 41
2025-09-17 46
註1:pl.Expr.dt.strftime()會於底層呼叫pl.Expr.dt.to_string()。前者命名比較偏Python風格,而後者則比較偏Rust。
註2:關於時間間隔,Polars提供了一系列的寫法:
shape: (12, 2)
┌──────┬────────────────────┐
│ rule ┆ representation │
│ --- ┆ --- │
│ str ┆ str │
╞══════╪════════════════════╡
│ 1ns ┆ 1 nanosecond │
│ 1us ┆ 1 microsecond │
│ 1ms ┆ 1 millisecond │
│ 1s ┆ 1 second │
│ 1m ┆ 1 minute │
│ 1h ┆ 1 hour │
│ 1d ┆ 1 calendar day │
│ 1w ┆ 1 calendar week │
│ 1mo ┆ 1 calendar month │
│ 1q ┆ 1 calendar quarter │
│ 1y ┆ 1 calendar year │
│ 1i ┆ 1 index count │
└──────┴────────────────────┘